<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h3 data-lake-id="a047b772" id="a047b772"><span data-lake-id="u0645adbc" id="u0645adbc">现象</span></h3>
  <p data-lake-id="u3137326b" id="u3137326b"><br></p>
  <p data-lake-id="ueb2bd672" id="ueb2bd672"><span data-lake-id="u6c1d7b93" id="u6c1d7b93">某天晚上，同事正在发布，突然线上大量报警，很多是关于数据库死锁的，报警提示信息如下：</span></p>
  <p data-lake-id="u41938c65" id="u41938c65"><br></p>
  <pre lang="java"><code>
{"errorCode":"SYSTEM_ERROR","errorMsg":"nested exception is org.apache.ibatis.exceptions.PersistenceException: 

Error updating database. Cause: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL] 

Deadlock found when trying to get lock; 

The error occurred while setting parameters\n### SQL: 

update fund_transfer_stream set gmt_modified=now(),state = ? where fund_transfer_order_no = ? and seller_id = ? and state = 'NEW'
</code></pre>
  <p data-lake-id="ufe63db89" id="ufe63db89"><br></p>
  <p data-lake-id="u5f1663e6" id="u5f1663e6"><span data-lake-id="u518bbd5d" id="u518bbd5d">通过报警，我们基本可以定位到发生死锁的数据库以及数据库表。先来介绍下本文案例中涉及到的数据库相关信息。</span></p>
  <p data-lake-id="u168f943e" id="u168f943e"><br></p>
  <h3 data-lake-id="385a0f61" id="385a0f61"><span data-lake-id="u2d3c6cdf" id="u2d3c6cdf">背景情况</span></h3>
  <p data-lake-id="ub5610085" id="ub5610085"><br></p>
  <p data-lake-id="u24916a37" id="u24916a37"><span data-lake-id="u42372fe4" id="u42372fe4">我们使用的数据库是Mysql 5.7，引擎是InnoDB，事务隔离级别是READ-COMMITED。</span></p>
  <p data-lake-id="u8fab5e28" id="u8fab5e28"><br></p>
  <p data-lake-id="ufd162195" id="ufd162195"><span data-lake-id="ub2215092" id="ub2215092">数据库版本查询方法：</span></p>
  <p data-lake-id="u73ec6124" id="u73ec6124"><br></p>
  <pre lang="java"><code>
SELECT version();
</code></pre>
  <p data-lake-id="uc3f712c1" id="uc3f712c1"><br></p>
  <p data-lake-id="u0c31cb76" id="u0c31cb76"><span data-lake-id="ua0f073b2" id="ua0f073b2">引擎查询方法：</span></p>
  <p data-lake-id="u062f09e9" id="u062f09e9"><br></p>
  <pre lang="java"><code>
show create table fund_transfer_stream;
</code></pre>
  <p data-lake-id="u5877e8f6" id="u5877e8f6"><br></p>
  <p data-lake-id="ufe2d08f9" id="ufe2d08f9"><span data-lake-id="ue2534d3a" id="ue2534d3a">建表语句中会显示存储引擎信息，形如：</span><code data-lake-id="uaf751d34" id="uaf751d34"><span data-lake-id="u0fc489c2" id="u0fc489c2">ENGINE=InnoDB</span></code></p>
  <p data-lake-id="ud00977fb" id="ud00977fb"><br></p>
  <p data-lake-id="u8cb0d9fe" id="u8cb0d9fe"><span data-lake-id="u1a2ad88d" id="u1a2ad88d">事务隔离级别查询方法：</span></p>
  <p data-lake-id="u49dda066" id="u49dda066"><br></p>
  <pre lang="java"><code>
select @@tx_isolation;
</code></pre>
  <p data-lake-id="ua5c7147b" id="ua5c7147b"><br></p>
  <p data-lake-id="uf38c2eb9" id="uf38c2eb9"><span data-lake-id="u73a1bd4f" id="u73a1bd4f">事务隔离级别设置方法（只对当前Session生效）：</span></p>
  <p data-lake-id="u3307f060" id="u3307f060"><br></p>
  <pre lang="java"><code>
set session transaction isolation level read committed;
</code></pre>
  <p data-lake-id="u43517dfa" id="u43517dfa"><br></p>
  <p data-lake-id="ua621944e" id="ua621944e"><span data-lake-id="u67497c2d" id="u67497c2d">PS：注意，如果数据库是分库的，以上几条SQL语句需要在单库上执行，不要在逻辑库执行。</span></p>
  <p data-lake-id="u63629f08" id="u63629f08"><br></p>
  <p data-lake-id="u4e649e2e" id="u4e649e2e"><span data-lake-id="u233d537f" id="u233d537f">发生死锁的表结构及索引情况（隐去了部分无关字段和索引）：</span></p>
  <p data-lake-id="uc4ef30fa" id="uc4ef30fa"><br></p>
  <pre lang="java"><code>
CREATE TABLE `fund_transfer_stream` ( 
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `gmt_create` datetime NOT NULL COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间', 
  `pay_scene_name` varchar(256) NOT NULL COMMENT '支付场景名称', 
  `pay_scene_version` varchar(256) DEFAULT NULL COMMENT '支付场景版本',
  `identifier` varchar(256) NOT NULL COMMENT '唯一性标识',
  `seller_id` varchar(64) NOT NULL COMMENT '卖家Id',
  `state` varchar(64) DEFAULT NULL COMMENT '状态', `fund_transfer_order_no` varchar(256) 
  DEFAULT NULL COMMENT '资金平台返回的状态', 
  PRIMARY KEY (`id`),
  KEY `idx_seller` (`seller_id`),
  KEY `idx_seller_transNo` (`seller_id`,`fund_transfer_order_no`(20))
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金流水';
</code></pre>
  <p data-lake-id="uc60885a7" id="uc60885a7"><br></p>
  <p data-lake-id="u552d449d" id="u552d449d"><span data-lake-id="u1c1fa71b" id="u1c1fa71b">该数据库共有三个索引，1个聚簇索引（主键索引），2个非聚簇索（非主键索引）引。</span></p>
  <p data-lake-id="u5fe620ce" id="u5fe620ce"><br></p>
  <p data-lake-id="u0ad9a611" id="u0ad9a611"><span data-lake-id="u0a47ea05" id="u0a47ea05">聚簇索引：</span></p>
  <p data-lake-id="u01a7458a" id="u01a7458a"><br></p>
  <pre lang="java"><code>
PRIMARY KEY (`id`)
</code></pre>
  <p data-lake-id="ubdb59123" id="ubdb59123"><br></p>
  <p data-lake-id="uf9c5f8ce" id="uf9c5f8ce"><span data-lake-id="u17e07db9" id="u17e07db9">非聚簇索引：</span></p>
  <p data-lake-id="ua54371a5" id="ua54371a5"><br></p>
  <pre lang="java"><code>
KEY `idx_seller` (`seller_id`),

KEY `idx_seller_transNo` (`seller_id`,`fund_transfer_order_no`(20))
</code></pre>
  <p data-lake-id="ucd915f6b" id="ucd915f6b"><br></p>
  <p data-lake-id="u12b3d318" id="u12b3d318"><span data-lake-id="ucf78281e" id="ucf78281e">以上两个索引，其实idx_seller_transNo已经覆盖到了idx_seller，由于历史原因，因为该表以seller_id分表，所以是先有的idx_seller，后有的idx_seller_transNo</span></p>
  <p data-lake-id="ufa82fc9d" id="ufa82fc9d"><br></p>
  <h3 data-lake-id="7583b4a5" id="7583b4a5"><span data-lake-id="u562ffd54" id="u562ffd54">死锁日志</span></h3>
  <p data-lake-id="u853333f9" id="u853333f9"><br></p>
  <p data-lake-id="u2eedb620" id="u2eedb620"><span data-lake-id="ub9979816" id="ub9979816">当数据库发生死锁时，可以通过以下命令获取死锁日志：</span></p>
  <p data-lake-id="u4b50cb82" id="u4b50cb82"><br></p>
  <pre lang="java"><code>
show engine innodb status
</code></pre>
  <p data-lake-id="uf73d363a" id="uf73d363a"><br></p>
  <p data-lake-id="ue134efb7" id="ue134efb7"><span data-lake-id="ud618eb30" id="ud618eb30">发生死锁，第一时间查看死锁日志，得到死锁日志内容如下：</span></p>
  <p data-lake-id="ue47022c6" id="ue47022c6"><br></p>
  <pre lang="java"><code>
Transactions deadlock detected, dumping detailed information.
2019-03-19T21:44:23.516263+08:00 5877341 [Note] InnoDB: 

*** (1) TRANSACTION:
TRANSACTION 173268495, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 304 lock struct(s), heap size 41168, 6 row lock(s), undo log entries 1
MySQL thread id 5877358, OS thread handle 47356539049728, query id 557970181 11.183.244.150 fin_instant_app updating

update `fund_transfer_stream` set `gmt_modified` = NOW(), `state` = 'PROCESSING' where ((`state` = 'NEW') AND (`seller_id` = '38921111') AND (`fund_transfer_order_no` = '99010015000805619031958363857'))
2019-03-19T21:44:23.516321+08:00 5877341 [Note] InnoDB: 

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 173 page no 13726 n bits 248 index idx_seller_transNo of table `xxx`.`fund_transfer_stream` trx id 173268495 lock_mode X locks rec but not gap
Record lock, heap no 168 PHYSICAL RECORD: n_fields 3; compact format; info bits 0

2019-03-19T21:44:23.516565+08:00 5877341 [Note] InnoDB: 

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 173 page no 12416 n bits 128 index PRIMARY of table `xxx`.`fund_transfer_stream` trx id 173268495 lock_mode X locks rec but not gap waiting
Record lock, heap no 56 PHYSICAL RECORD: n_fields 17; compact format; info bits 0
2019-03-19T21:44:23.517793+08:00 5877341 [Note] InnoDB: 

*** (2) TRANSACTION:
TRANSACTION 173268500, ACTIVE 0 sec fetching rows, thread declared inside InnoDB 81
mysql tables in use 1, locked 1
302 lock struct(s), heap size 41168, 2 row lock(s), undo log entries 1
MySQL thread id 5877341, OS thread handle 47362313119488, query id 557970189 11.131.81.107 fin_instant_app updating

update `fund_transfer_stream_0056` set `gmt_modified` = NOW(), `state` = 'PROCESSING' where ((`state` = 'NEW') AND (`seller_id` = '38921111') AND (`fund_transfer_order_no` = '99010015000805619031957477256'))
2019-03-19T21:44:23.517855+08:00 5877341 [Note] InnoDB: 

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 173 page no 12416 n bits 128 index PRIMARY of table `fin_instant_0003`.`fund_transfer_stream_0056` trx id 173268500 lock_mode X locks rec but not gap
Record lock, heap no 56 PHYSICAL RECORD: n_fields 17; compact format; info bits 0

2019-03-19T21:44:23.519053+08:00 5877341 [Note] InnoDB: 

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 173 page no 13726 n bits 248 index idx_seller_transNo of table `fin_instant_0003`.`fund_transfer_stream_0056` trx id 173268500 lock_mode X locks rec but not gap waiting
Record lock, heap no 168 PHYSICAL RECORD: n_fields 3; compact format; info bits 0

2019-03-19T21:44:23.519297+08:00 5877341 [Note] InnoDB: *** WE ROLL BACK TRANSACTION (2)
</code></pre>
  <p data-lake-id="u1a0da20d" id="u1a0da20d"><br></p>
  <p data-lake-id="uafb33861" id="uafb33861"><span data-lake-id="ub04a79ab" id="ub04a79ab">简单解读一下死锁日志，可以得到以下信息：</span></p>
  <p data-lake-id="u53de33cf" id="u53de33cf"><br></p>
  <p data-lake-id="u82ed404c" id="u82ed404c"><span data-lake-id="u77b067a5" id="u77b067a5">1、导致死锁的两条SQL语句分别是：</span></p>
  <p data-lake-id="u43aac18b" id="u43aac18b"><br></p>
  <pre lang="java"><code>
update `fund_transfer_stream_0056` 
set `gmt_modified` = NOW(), `state` = 'PROCESSING' 
where ((`state` = 'NEW') AND (`seller_id` = '38921111') AND (`fund_transfer_order_no` = '99010015000805619031957477256'))
</code></pre>
  <p data-lake-id="uab378e31" id="uab378e31"><br></p>
  <p data-lake-id="u6a2f8594" id="u6a2f8594"><span data-lake-id="ua8281c3f" id="ua8281c3f">和</span></p>
  <p data-lake-id="u5e2faff4" id="u5e2faff4"><br></p>
  <pre lang="java"><code>
update `fund_transfer_stream_0056` 
set `gmt_modified` = NOW(), `state` = 'PROCESSING' 
where ((`state` = 'NEW') AND (`seller_id` = '38921111') AND (`fund_transfer_order_no` = '99010015000805619031958363857'))
</code></pre>
  <p data-lake-id="uf99f4c20" id="uf99f4c20"><br></p>
  <p data-lake-id="u8aa46f99" id="u8aa46f99"><span data-lake-id="u29277113" id="u29277113">2、事务1，持有索引idx_seller_transNo的锁，在等待获取PRIMARY的锁。</span></p>
  <p data-lake-id="u5274ce1c" id="u5274ce1c"><br></p>
  <p data-lake-id="u08a42aff" id="u08a42aff"><span data-lake-id="ue7933a17" id="ue7933a17">3、事务2，持有PRIMARY的锁，在等待获取idx_seller_transNo的锁。</span></p>
  <p data-lake-id="ua6ffe721" id="ua6ffe721"><br></p>
  <p data-lake-id="u02abf841" id="u02abf841"><span data-lake-id="ub30c2013" id="ub30c2013">4、因事务1和事务2之间发生循环等待，故发生死锁。</span></p>
  <p data-lake-id="uc910aa7d" id="uc910aa7d"><br></p>
  <p data-lake-id="u4cde7516" id="u4cde7516"><span data-lake-id="u3af03174" id="u3af03174">5、事务1和事务2当前持有的锁均为：</span><code data-lake-id="u2d1d15b9" id="u2d1d15b9"><span data-lake-id="u36b61dc2" id="u36b61dc2">lock_mode X locks rec but not gap</span></code></p>
  <p data-lake-id="u572eeb97" id="u572eeb97"><br></p>
  <p data-lake-id="u16c6ff8f" id="u16c6ff8f"><span data-lake-id="u036dad55" id="u036dad55">两个事务对记录加的都是X 锁，No Gap锁，即对当行记录加锁，并未加间隙锁。</span></p>
  <p data-lake-id="u903a8867" id="u903a8867"><br></p>
  <blockquote data-lake-id="uc62e05fd" id="uc62e05fd">
   <p data-lake-id="uf0f67f17" id="uf0f67f17"><span data-lake-id="u57ca9032" id="u57ca9032">X锁：排他锁、又称写锁。若事务T对数据对象A加上X锁，事务T可以读A也可以修改A，其他事务不能再对A加任何锁，直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。</span></p>
   <p data-lake-id="u3c69b78d" id="u3c69b78d"><span data-lake-id="u48e92f9c" id="u48e92f9c"> </span></p>
   <p data-lake-id="ub4d410f8" id="ub4d410f8"><span data-lake-id="uac5d2557" id="uac5d2557">与之对应的是S锁：共享锁，又称读锁，若事务T对数据对象A加上S锁，则事务T可以读A但不能修改A，其他事务只能再对A加S锁，而不能加X锁，直到T释放A上的S锁。这保证了其他事务可以读A，但在T释放A上的S锁之前不能对A做任何修改。</span></p>
   <p data-lake-id="u380be673" id="u380be673"><span data-lake-id="ufdacd839" id="ufdacd839"> </span></p>
   <p data-lake-id="u2338e5a4" id="u2338e5a4"><span data-lake-id="uafd2865b" id="uafd2865b">Gap Lock：间隙锁，锁定一个范围，但不包括记录本身。GAP锁的目的，是为了防止同一事务的两次当前读，出现幻读的情况。</span></p>
   <p data-lake-id="u002803b7" id="u002803b7"><span data-lake-id="u0f9befd1" id="u0f9befd1"> </span></p>
   <p data-lake-id="u2c3791e5" id="u2c3791e5"><span data-lake-id="u88127df3" id="u88127df3">Next-Key Lock：1+2，锁定一个范围，并且锁定记录本身。对于行的查询，都是采用该方法，主要目的是解决幻读的问题。</span></p>
  </blockquote>
  <p data-lake-id="u5700042b" id="u5700042b"><br></p>
  <p data-lake-id="u7d0b7f9f" id="u7d0b7f9f"><span data-lake-id="udf4107a9" id="udf4107a9">详见：</span><a href="https://www.cnblogs.com/zhoujinyi/p/3435982.html" target="_blank" data-lake-id="udd8aa31c" id="udd8aa31c"><span data-lake-id="uee887f2e" id="uee887f2e">https://www.cnblogs.com/zhoujinyi/p/3435982.html</span></a><span data-lake-id="ubab90373" id="ubab90373"> 、 </span><a href="https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html" target="_blank" data-lake-id="u7e820c43" id="u7e820c43"><span data-lake-id="u6bc3691a" id="u6bc3691a">https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html</span></a></p>
  <p data-lake-id="u617d43bf" id="u617d43bf"><br></p>
  <h3 data-lake-id="6e621f2f" id="6e621f2f"><span data-lake-id="u59ddfb2a" id="u59ddfb2a">问题排查</span></h3>
  <p data-lake-id="u99a8b968" id="u99a8b968"><br></p>
  <p data-lake-id="u0bb37123" id="u0bb37123"><span data-lake-id="u51c535a4" id="u51c535a4">根据我们目前已知的数据库相关信息，以及死锁的日志，我们基本可以做一些简单的判定。</span></p>
  <p data-lake-id="u5c3517e1" id="u5c3517e1"><br></p>
  <p data-lake-id="ud6035a8c" id="ud6035a8c"><span data-lake-id="u704652fc" id="u704652fc">首先，</span><strong><span data-lake-id="u3aa1026b" id="u3aa1026b">此次死锁一定是和Gap锁以及Next-Key Lock没有关系的</span></strong><span data-lake-id="u4b1fc8c0" id="u4b1fc8c0">。因为我们的数据库隔离级别是RC（READ-COMMITED）的，这种隔离级别是不会添加Gap锁的。前面的死锁日志也提到这一点。</span></p>
  <p data-lake-id="u52d68df3" id="u52d68df3"><br></p>
  <p data-lake-id="u975d820c" id="u975d820c"><span data-lake-id="u46adb231" id="u46adb231">然后，就要翻代码了，看看我们的代码中事务到底是怎么做的。核心代码及SQL如下：</span></p>
  <p data-lake-id="ud36b6a2f" id="ud36b6a2f"><br></p>
  <pre lang="java"><code>
@Transactional(rollbackFor = Exception.class)
public int doProcessing(String sellerId, Long id, String fundTransferOrderNo) {
    fundTreansferStreamDAO.updateFundStreamId(sellerId, id, fundTransferOrderNo);
    return fundTreansferStreamDAO.updateStatus(sellerId, fundTransferOrderNo, FundTransferStreamState.PROCESSING.name());
}
</code></pre>
  <p data-lake-id="u9a35d4a9" id="u9a35d4a9"><br></p>
  <p data-lake-id="ua98fc6a7" id="ua98fc6a7"><span data-lake-id="u31f832e1" id="u31f832e1">该代码的目的是先后修改同一条记录的两个不同字段，updateFundStreamId SQL：</span></p>
  <p data-lake-id="ufbca0d07" id="ufbca0d07"><br></p>
  <pre lang="java"><code>
update fund_transfer_stream
        set gmt_modified=now(),fund_transfer_order_no = #{fundTransferOrderNo}
        where id = #{id} and seller_id = #{sellerId}
</code></pre>
  <p data-lake-id="uc2a2a795" id="uc2a2a795"><br></p>
  <p data-lake-id="u38c972b3" id="u38c972b3"><span data-lake-id="u4b8b25b9" id="u4b8b25b9">updateStatus SQL：</span></p>
  <p data-lake-id="ubbc94d49" id="ubbc94d49"><br></p>
  <pre lang="java"><code>
update fund_transfer_stream
    set gmt_modified=now(),state = #{state}
    where fund_transfer_order_no = #{fundTransferOrderNo} and seller_id = #{sellerId}
    and state = 'NEW'
</code></pre>
  <p data-lake-id="ueeae3925" id="ueeae3925"><br></p>
  <p data-lake-id="u4ddcbf68" id="u4ddcbf68"><span data-lake-id="ub17c56de" id="ub17c56de">可以看到，我们的同一个事务中执行了两条Update语句，这里分别查看下两条SQL的执行计划：</span></p>
  <p data-lake-id="uc3aa43d4" id="uc3aa43d4"><br></p>
  <p data-lake-id="ue8e6789a" id="ue8e6789a"><img src="http://www.hollischuang.com/wp-content/uploads/2019/03/15540129755807.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_33%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="ubdf30440" id="ubdf30440">​</span></p>
  <p data-lake-id="u46fda6f2" id="u46fda6f2"><br></p>
  <p data-lake-id="u3179ce2f" id="u3179ce2f"><span data-lake-id="u67ddd7c4" id="u67ddd7c4">updateFundStreamId执行的时候使用到的是PRIMARY索引。</span></p>
  <p data-lake-id="u9655a7e6" id="u9655a7e6"><br></p>
  <p data-lake-id="udccf6c45" id="udccf6c45"><img src="http://www.hollischuang.com/wp-content/uploads/2019/03/15540130212992.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_45%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="ufcb31233" id="ufcb31233">​</span></p>
  <p data-lake-id="udbd90e15" id="udbd90e15"><br></p>
  <p data-lake-id="u0461a923" id="u0461a923"><span data-lake-id="uaae0e448" id="uaae0e448">updateStatus执行的时候使用到的是idx_seller_transNo索引。</span></p>
  <p data-lake-id="u384313e8" id="u384313e8"><br></p>
  <blockquote data-lake-id="u1a5b60e1" id="u1a5b60e1">
   <p data-lake-id="u53a22f1d" id="u53a22f1d"><span data-lake-id="u13a88ddd" id="u13a88ddd">通过执行计划，我们发现updateStatus其实是有两个索引可以用的，执行的时候真正使用的是idx_seller_transNo索引。这是因为</span><strong><span data-lake-id="ud8eda51c" id="ud8eda51c">MySQL查询优化器是基于代价（cost-based）的查询方式。因此，在查询过程中，最重要的一部分是根据查询的SQL语句，依据多种索引，计算查询需要的代价，从而选择最优的索引方式生成查询计划。</span></strong></p>
   <p data-lake-id="ud6c7b43a" id="ud6c7b43a"><span data-lake-id="u395d07fe" id="u395d07fe"> </span></p>
   <p data-lake-id="u97919ba7" id="u97919ba7"><span data-lake-id="u0c57cbda" id="u0c57cbda">我们查询执行计划是在死锁发生之后做的，事后查询的执行计划和发生死锁那一刻的索引使用情况并不一定相同的。但是，我们结合死锁日志，也可以定位到以上两条SQL语句执行的时候使用到的索引。即</span><strong><span data-lake-id="u51501408" id="u51501408">updateFundStreamId执行的时候使用到的是PRIMARY索引，updateStatus执行的时候使用到的是idx_seller_transNo索引</span></strong><span data-lake-id="uae5949d1" id="uae5949d1">。</span></p>
  </blockquote>
  <p data-lake-id="uaff7610b" id="uaff7610b"><br></p>
  <p data-lake-id="ud20d3842" id="ud20d3842"><span data-lake-id="u30a03d02" id="u30a03d02">有了以上这些已知信息，我们就可以开始排查死锁原因及其背后的原理了。通过分析死锁日志，再结合我们的代码以及数据库建表语句，我们发现主要问题出在我们的idx_seller_transNo索引上面：</span></p>
  <p data-lake-id="u40fa2b2d" id="u40fa2b2d"><br></p>
  <pre lang="java"><code>
 KEY `idx_seller_transNo` (`seller_id`,`fund_transfer_order_no`(20))
</code></pre>
  <p data-lake-id="u0aba3c95" id="u0aba3c95"><br></p>
  <p data-lake-id="u9351b9ae" id="u9351b9ae"><span data-lake-id="ub7417233" id="ub7417233">索引创建语句中，我们使用了前缀索引，为了节约索引空间，提高索引效率，我们只选择了fund_transfer_order_no字段的前20位作为索引值。</span></p>
  <p data-lake-id="ua1ab936f" id="ua1ab936f"><br></p>
  <p data-lake-id="u8e34489c" id="u8e34489c"><span data-lake-id="u295e48e1" id="u295e48e1">因为fund_transfer_order_no只是普通索引，而非唯一性索引。又因为在一种特殊情况下，会有同一个用户的两个fund_transfer_order_no的前20位相同，这就导致两条不同的记录的索引值一样（因为seller_id 和fund_transfer_order_no(20)都相同 ）。</span></p>
  <p data-lake-id="u9ea18b93" id="u9ea18b93"><br></p>
  <p data-lake-id="u7789db25" id="u7789db25"><span data-lake-id="ue55a1f5c" id="ue55a1f5c">就如本文中的例子，发生死锁的两条记录的fund_transfer_order_no字段的值：99010015000805619031958363857和99010015000805619031957477256这两个就是前20位相同的。</span></p>
  <p data-lake-id="u26204cf7" id="u26204cf7"><br></p>
  <p data-lake-id="u7c374a0e" id="u7c374a0e"><img src="http://www.hollischuang.com/wp-content/uploads/2019/03/15540203229844.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_27%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="u22a16893" id="u22a16893">​</span></p>
  <p data-lake-id="u84bb369b" id="u84bb369b"><br></p>
  <p data-lake-id="u143cc2b5" id="u143cc2b5"><span data-lake-id="u6ca9692e" id="u6ca9692e">那么为什么fund_transfer_order_no的前20位相同会导致死锁呢？</span></p>
  <p data-lake-id="u12d220cc" id="u12d220cc"><br></p>
  <h3 data-lake-id="b36b8603" id="b36b8603"><span data-lake-id="u8e366600" id="u8e366600">加锁原理</span></h3>
  <p data-lake-id="ue5d7380f" id="ue5d7380f"><br></p>
  <p data-lake-id="u5de60e58" id="u5de60e58"><span data-lake-id="u2e6d8245" id="u2e6d8245">我们就拿本次的案例来看一下MySql数据库加锁的原理是怎样的，本文的死锁背后又发生了什么。</span></p>
  <p data-lake-id="u18a3fac0" id="u18a3fac0"><br></p>
  <p data-lake-id="u146bc810" id="u146bc810"><span data-lake-id="u564a0c6c" id="u564a0c6c">我们在数据库上模拟死锁场景，执行顺序如下：</span></p>
  <table data-lake-id="wMMCm" id="wMMCm" margin="true" class="lake-table" style="width: 750px">
   <colgroup>
    <col width="250">
    <col width="250">
    <col width="250">
   </colgroup>
   <tbody>
    <tr data-lake-id="ue7b48a86" id="ue7b48a86">
     <td data-lake-id="uc0094a1c" id="uc0094a1c" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="u0bdec5a0" id="u0bdec5a0" style="text-align: center"><span data-lake-id="ub7b06750" id="ub7b06750" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">事务1</span></p></td>
     <td data-lake-id="u001e571f" id="u001e571f" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="u12af08fd" id="u12af08fd" style="text-align: center"><span data-lake-id="u91333b3a" id="u91333b3a" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">事务2</span></p></td>
     <td data-lake-id="u0a6ed923" id="u0a6ed923" style="background-color: rgb(249, 249, 249)">
      <p data-lake-id="u3181d28a" id="u3181d28a" style="text-align: center"><span data-lake-id="u9f9477ba" id="u9f9477ba" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">执行结果</span></p></td>
    </tr>
    <tr data-lake-id="u6cccebd2" id="u6cccebd2">
     <td data-lake-id="ua4743c39" id="ua4743c39">
      <p data-lake-id="u7d8442a2" id="u7d8442a2" style="text-align: justify"><span data-lake-id="u07ddd251" id="u07ddd251" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">begin</span></p></td>
     <td data-lake-id="u0ce7f4ca" id="u0ce7f4ca"></td>
     <td data-lake-id="u4f2d55e1" id="u4f2d55e1"></td>
    </tr>
    <tr data-lake-id="ua13bac38" id="ua13bac38">
     <td data-lake-id="ud9e05f6d" id="ud9e05f6d">
      <p data-lake-id="u36d3e8e7" id="u36d3e8e7" style="text-align: justify"><span data-lake-id="u604641ec" id="u604641ec" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">update fund_transfer_stream set gmt_modified=now(),fund_transfer_order_no = '99010015000805619031958363857' where id = 1 and seller_id = 3111095611;</span></p></td>
     <td data-lake-id="ufa180554" id="ufa180554"></td>
     <td data-lake-id="ucc4bcc73" id="ucc4bcc73">
      <p data-lake-id="udc643881" id="udc643881" style="text-align: justify"><span data-lake-id="u8a0d1720" id="u8a0d1720" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">执行成功</span></p></td>
    </tr>
    <tr data-lake-id="u845ce60d" id="u845ce60d">
     <td data-lake-id="u0a85bc87" id="u0a85bc87"></td>
     <td data-lake-id="ufdb7bc20" id="ufdb7bc20">
      <p data-lake-id="u71533a9b" id="u71533a9b" style="text-align: justify"><span data-lake-id="udad9f35b" id="udad9f35b" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">begin</span></p></td>
     <td data-lake-id="u08c1c489" id="u08c1c489"></td>
    </tr>
    <tr data-lake-id="u1151955a" id="u1151955a">
     <td data-lake-id="u87c48bf1" id="u87c48bf1"></td>
     <td data-lake-id="u50cab43a" id="u50cab43a">
      <p data-lake-id="u2a135636" id="u2a135636" style="text-align: justify"><span data-lake-id="u0afef589" id="u0afef589" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">update fund_transfer_stream set gmt_modified=now(),fund_transfer_order_no = '99010015000805619031957477256' where id = 2 and seller_id = 3111095611;</span></p></td>
     <td data-lake-id="ufbfb123a" id="ufbfb123a">
      <p data-lake-id="uf01d220a" id="uf01d220a" style="text-align: justify"><span data-lake-id="u3576db4e" id="u3576db4e" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">执行成功</span></p></td>
    </tr>
    <tr data-lake-id="ub460e813" id="ub460e813">
     <td data-lake-id="u4d8427b9" id="u4d8427b9">
      <p data-lake-id="ud55c0c4d" id="ud55c0c4d" style="text-align: justify"><span data-lake-id="u2278158d" id="u2278158d" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">update fund_transfer_stream set gmt_modified = NOW(), state = 'PROCESSING' where ((state = 'NEW') AND (seller_id = '3111095611') AND (fund_transfer_order_no = '99010015000805619031958363857'));</span></p></td>
     <td data-lake-id="ub875427c" id="ub875427c"></td>
     <td data-lake-id="u908a70cb" id="u908a70cb">
      <p data-lake-id="ub3bf81af" id="ub3bf81af" style="text-align: justify"><span data-lake-id="u97dc7f12" id="u97dc7f12" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">阻塞</span></p></td>
    </tr>
    <tr data-lake-id="u3e45f364" id="u3e45f364">
     <td data-lake-id="uf8fbf39d" id="uf8fbf39d"></td>
     <td data-lake-id="uc75a5a88" id="uc75a5a88">
      <p data-lake-id="ub5e6eb61" id="ub5e6eb61" style="text-align: justify"><span data-lake-id="ua3ef74ec" id="ua3ef74ec" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">update fund_transfer_stream set gmt_modified = NOW(), state = 'PROCESSING' where ((state = 'NEW') AND (seller_id = '3111095611') AND (fund_transfer_order_no = '99010015000805619031957477256'));</span></p></td>
     <td data-lake-id="ufd30b3c2" id="ufd30b3c2">
      <p data-lake-id="ua5ab4cab" id="ua5ab4cab" style="text-align: justify"><span data-lake-id="udb05d72d" id="udb05d72d" class="lake-fontsize-9" style="color: rgb(85, 85, 85)">死锁</span></p></td>
    </tr>
   </tbody>
  </table>
  <p data-lake-id="u5cabd53a" id="u5cabd53a"><span data-lake-id="uab69407a" id="uab69407a">我们知道，</span><strong><span data-lake-id="u3e997f51" id="u3e997f51">在MySQL中，行级锁并不是直接锁记录，而是锁索引。索引分为主键索引和非主键索引两种，如果一条sql语句操作了主键索引，MySQL就会锁定这条主键索引；如果一条语句操作了非主键索引，MySQL会先锁定该非主键索引，再锁定相关的主键索引。</span></strong></p>
  <p data-lake-id="u66bcff07" id="u66bcff07"><br></p>
  <blockquote data-lake-id="u5315e1bd" id="u5315e1bd">
   <p data-lake-id="u2445a234" id="u2445a234"><span data-lake-id="u5c86803f" id="u5c86803f">主键索引的叶子节点存的是整行数据。在InnoDB中，主键索引也被称为聚簇索引（clustered index）</span></p>
   <p data-lake-id="u9acaf900" id="u9acaf900"><span data-lake-id="uc7b9c40e" id="uc7b9c40e"> </span></p>
   <p data-lake-id="ub69a87b9" id="ub69a87b9"><span data-lake-id="u455d39bf" id="u455d39bf">非主键索引的叶子节点的内容是主键的值，在InnoDB中，非主键索引也被称为非聚簇索引（secondary index）</span></p>
  </blockquote>
  <p data-lake-id="u1f7840cc" id="u1f7840cc"><br></p>
  <p data-lake-id="u13c5024d" id="u13c5024d"><span data-lake-id="ue551c664" id="ue551c664">所以，本文的示例中涉及到的索引结构（索引是B+树，简化成表格了）如图：</span></p>
  <p data-lake-id="uf3ca534f" id="uf3ca534f"><br></p>
  <p data-lake-id="ua999c97a" id="ua999c97a"><img src="http://www.hollischuang.com/wp-content/uploads/2019/03/15540203644300.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_27%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="uee3a73ac" id="uee3a73ac">​</span></p>
  <p data-lake-id="u3a7b98a3" id="u3a7b98a3"><br></p>
  <p data-lake-id="uf2269f22" id="uf2269f22"><span data-lake-id="ua77dc4b5" id="ua77dc4b5">死锁的发生与否，并不在于事务中有多少条SQL语句，</span><strong><span data-lake-id="u9a8e0db9" id="u9a8e0db9">死锁的关键在于：两个(或以上)的Session加锁的顺序不一致。</span></strong><span data-lake-id="uc1319ad4" id="uc1319ad4">那么接下来就看下上面的例子中两个事务的加锁顺序是怎样的：</span></p>
  <p data-lake-id="uaca607c7" id="uaca607c7"><br></p>
  <p data-lake-id="u442016d8" id="u442016d8"><img src="http://www.hollischuang.com/wp-content/uploads/2019/03/15540203894387.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_36%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="ud3744711" id="ud3744711">​</span></p>
  <p data-lake-id="u88474d3d" id="u88474d3d"><br></p>
  <p data-lake-id="ue32d5567" id="ue32d5567"><span data-lake-id="u10156324" id="u10156324">下图是分解图，每一条SQL执行的时候加锁情况：</span></p>
  <p data-lake-id="udf88a833" id="udf88a833"><br></p>
  <p data-lake-id="u2bba413a" id="u2bba413a"><img src="http://www.hollischuang.com/wp-content/uploads/2019/03/15540204145590.jpg?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_46%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"><span data-lake-id="u421e43d5" id="u421e43d5">​</span></p>
  <p data-lake-id="u8cd20a05" id="u8cd20a05"><br></p>
  <p data-lake-id="u2faa8df4" id="u2faa8df4"><span data-lake-id="ua681f174" id="ua681f174">结合以上两张图，我们发现了导致死锁的原因： 事务1执行update1占用PRIMARY = 1的锁 ——&gt; 事务2执行update1 占有PRIMARY = 2的锁； 事务1执行update2占有idx_seller_transNo = (3111095611，99010015000805619031)的锁，尝试占有PRIMARY = 2锁失败（阻塞）； 事务2执行update2尝试占有idx_seller_transNo = (3111095611，99010015000805619031)的锁失败（死锁）；</span></p>
  <p data-lake-id="u05af792b" id="u05af792b"><br></p>
  <blockquote data-lake-id="ub82a84dc" id="ub82a84dc">
   <p data-lake-id="u5708f848" id="u5708f848"><span data-lake-id="uf9c4a197" id="uf9c4a197">事务在以非主键索引为where条件进行Update的时候，会先对该非主键索引加锁，然后再查询该非主键索引对应的主键索引都有哪些，再对这些主键索引进行加锁。）</span></p>
  </blockquote>
  <p data-lake-id="u555bba97" id="u555bba97"><br></p>
  <h3 data-lake-id="957a228f" id="957a228f"><span data-lake-id="u7131864c" id="u7131864c">解决方法</span></h3>
  <p data-lake-id="u526ff74c" id="u526ff74c"><br></p>
  <p data-lake-id="ua37a036c" id="ua37a036c"><span data-lake-id="u5d8483dd" id="u5d8483dd">至此，我们分析清楚了导致死锁的根本原理以及其背后的原理。那么这个问题解决起来就不难了。</span></p>
  <p data-lake-id="ubcba8293" id="ubcba8293"><br></p>
  <p data-lake-id="u1f3e0f75" id="u1f3e0f75"><span data-lake-id="u4e2cc295" id="u4e2cc295">可以从两方面入手，分别是修改索引和修改代码（包含SQL语句）。</span></p>
  <p data-lake-id="u34dda66e" id="u34dda66e"><br></p>
  <p data-lake-id="u1d7683e5" id="u1d7683e5"><span data-lake-id="u0c6b7a89" id="u0c6b7a89">修改索引：只要我们把前缀索引 idx_seller_transNo中fund_transfer_order_no的前缀长度修改下就可以了。比如改成50。即可避免死锁。</span></p>
  <p data-lake-id="u99764e55" id="u99764e55"><br></p>
  <p data-lake-id="ucf95f63e" id="ucf95f63e"><span data-lake-id="u3566b618" id="u3566b618">但是，改了idx_seller_transNo的前缀长度后，可以解决死锁的前提条件是update语句真正执行的时候，会用到fund_transfer_order_no索引。如果MySQL查询优化器在代价分析之后，决定使用索引 KEY idx_seller(seller_id)，那么还是会存在死锁问题。原理和本文类似。</span></p>
  <p data-lake-id="u89f276f2" id="u89f276f2"><br></p>
  <p data-lake-id="ua5ec031c" id="ua5ec031c"><span data-lake-id="u2047cad3" id="u2047cad3">所以，根本解决办法就是改代码：</span></p>
  <p data-lake-id="uda5c223c" id="uda5c223c"><br></p>
  <pre lang="java"><code>
* 所有update都通过主键ID进行。
* 在同一个事务中，避免出现多条update语句修改同一条记录。
</code></pre>
  <p data-lake-id="u0dfea555" id="u0dfea555"><br></p>
  <h3 data-lake-id="cb7fbe75" id="cb7fbe75"><span data-lake-id="u72d831ee" id="u72d831ee">总结与思考</span></h3>
  <p data-lake-id="u252770fe" id="u252770fe"><br></p>
  <p data-lake-id="ueb5900f5" id="ueb5900f5"><span data-lake-id="ud48b1445" id="ud48b1445">在死锁发生之后的一周内，我几乎每天都会抽空研究一会，问题早早的就定位到了，修改方案也有了，但是其中原理一直没搞清楚。</span></p>
  <p data-lake-id="ud26dcc8f" id="ud26dcc8f"><br></p>
  <p data-lake-id="u2c4e3275" id="u2c4e3275"><span data-lake-id="u1ba69836" id="u1ba69836">前前后后做过很多种推断及假设，又都被自己一次次推翻。最终还是要靠实践来验证自己的想法。于是我自己在本地安装了数据库，实战的做了些测试，并实时查看数据库锁情况。</span><code data-lake-id="u722adfce" id="u722adfce"><span data-lake-id="ua4fec239" id="ua4fec239">show engine innodb status ;</span></code><span data-lake-id="u5cc3789b" id="u5cc3789b">可以查看锁情况。最终才搞清楚原理。</span></p>
  <p data-lake-id="u334e3c83" id="u334e3c83"><br></p>
  <p data-lake-id="u8ca1181a" id="u8ca1181a"><span data-lake-id="u54e34724" id="u54e34724">简单说几点思考：</span></p>
  <p data-lake-id="u3075d8ac" id="u3075d8ac"><br></p>
  <p data-lake-id="u95c2b13c" id="u95c2b13c"><span data-lake-id="u889f4000" id="u889f4000">1、遇到问题，不要猜！！！亲手复现下问题，然后再来分析。</span></p>
  <p data-lake-id="u912800fd" id="u912800fd"><br></p>
  <p data-lake-id="u90bc1012" id="u90bc1012"><span data-lake-id="u487811af" id="u487811af">2、不要忽略上下文！！！我刚开始就是只关注死锁日志，一直忽略了代码中的事务其实还执行了另外一条SQL语句（updateFundStreamId）。</span></p>
  <p data-lake-id="ud78bfbd0" id="ud78bfbd0"><br></p>
  <p data-lake-id="uc782222c" id="uc782222c"><span data-lake-id="u714a7c0a" id="u714a7c0a">3、理论知识再充足，关键时刻不一定想的起来！！！</span></p>
  <p data-lake-id="u908285e3" id="u908285e3"><br></p>
  <p data-lake-id="uef3b1b08" id="uef3b1b08"><span data-lake-id="u63223f85" id="u63223f85">4、坑都是自己埋的！！！</span></p>
  <p data-lake-id="ua848690e" id="ua848690e"><br></p>
  <p data-lake-id="u04f18357" id="u04f18357"><br></p>
 </body>
</html>